/*
    Android Asynchronous Http Client
    Copyright (c) 2013 JIXIAOLONG <MicroJixl@gmail.com>

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
 */
package com.jxl.netframe.apachebase;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpVersion;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.SyncBasicHttpContext;

import android.text.TextUtils;

import com.jxl.netframe.RequestParameter;

/** 
 * @author JiXL
 * @time 2013-5-27 下午5:37:37
 */
public class ApacheHttpClient{
	private static final String VERSION = "1.0.0";

	private static final int DEFAULT_MAX_CONNECTIONS = 10;
	private static final int DEFAULT_SOCKET_TIMEOUT = 10 * 1000;
	private static final int DEFAULT_MAX_RETRIES = 5;
	private static final int DEFAULT_SOCKET_BUFFER_SIZE = 8192;
	private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
	private static final String ENCODING_GZIP = "gzip";

	private static int maxConnections = DEFAULT_MAX_CONNECTIONS;
	private static int socketTimeout = DEFAULT_SOCKET_TIMEOUT;

	private final DefaultHttpClient httpClient;
	private final HttpContext httpContext;
	private final HttpRequestRetryHandler retryHandler;
	private final Map<String, String> clientHeaderMap;

	public ApacheHttpClient(){
		BasicHttpParams httpParams = new BasicHttpParams();

		ConnManagerParams.setTimeout(httpParams, socketTimeout);
		ConnManagerParams.setMaxConnectionsPerRoute(httpParams, new ConnPerRouteBean(maxConnections));
		ConnManagerParams.setMaxTotalConnections(httpParams, DEFAULT_MAX_CONNECTIONS);

		HttpConnectionParams.setSoTimeout(httpParams, socketTimeout);
		HttpConnectionParams.setConnectionTimeout(httpParams, socketTimeout);
		HttpConnectionParams.setTcpNoDelay(httpParams, true);
		HttpConnectionParams.setSocketBufferSize(httpParams, DEFAULT_SOCKET_BUFFER_SIZE);

		HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1);
		HttpProtocolParams.setUserAgent(httpParams, String.format("android-async-http/%s", VERSION));
		HttpProtocolParams.setUseExpectContinue(httpParams, false);
		
		//this set avoid CircularRedirectException.
		httpParams.setBooleanParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
		
		SchemeRegistry schemeRegistry = new SchemeRegistry();
		schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
		//TODO may be need implement X509TrustManager interface.
		schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
		ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(httpParams, schemeRegistry);
		httpContext = new SyncBasicHttpContext(new BasicHttpContext());
		httpClient = new DefaultHttpClient(cm, httpParams);
		/**
		 * mark by JXL
		 * =================================================
		 * 发送请求拦截器。在发送请求前做一些操作，例如下面增加头信息
		 * =================================================
		 */
		httpClient.addRequestInterceptor(new HttpRequestInterceptor() {
			@Override
			public void process(HttpRequest request, HttpContext context) {
				if (!request.containsHeader(HEADER_ACCEPT_ENCODING)) {
					request.addHeader(HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
				}
				//time complexity n square of n.
//				for (String header : clientHeaderMap.keySet()) {
//					request.addHeader(header, clientHeaderMap.get(header));
//				}
				
				for (Iterator<Entry<String, String>> iterator = clientHeaderMap.entrySet().iterator();iterator.hasNext();) {
					Entry<String, String> entry = iterator.next();
					request.addHeader(entry.getKey(), entry.getValue());
				}
			}
		});
		/**
		 * mark by JXL
		 * ============================================================
		 * 响应请求拦截器。收到响应请求前做一些操作，下面主要是对包进行了GZIP处理
		 * ============================================================
		 */
		httpClient.addResponseInterceptor(new HttpResponseInterceptor() {
			@Override
			public void process(HttpResponse response, HttpContext context) {
				final HttpEntity entity = response.getEntity();
				if (entity == null) {
					return;
				}
				final Header encoding = entity.getContentEncoding();
				if (encoding != null) {
					for (HeaderElement element : encoding.getElements()) {
						if (element.getName().equalsIgnoreCase(ENCODING_GZIP)) {
							response.setEntity(new InflatingEntity(response.getEntity()));
							break;
						}
					}
				}
			}
		});
		retryHandler = new RetryHandler(DEFAULT_MAX_RETRIES);
		httpClient.setHttpRequestRetryHandler(retryHandler);
		clientHeaderMap = new HashMap<String, String>();
	}

	/**
	 * Sets the Proxy
	 * @param hostname  the hostname (IP or DNS我· name) 
	 * @param port  the port number. -1 indicates the scheme default port.  
	 */
	public void setProxy(String hostname, int port){
		HttpHost proxy = new HttpHost(hostname, port);
		HttpParams httpParams = this.httpClient.getParams();
		httpParams.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
	}

	/**
	 * Sets an optional CookieStore to use when making requests
	 * @param cookieStore The CookieStore implementation to use, usually an instance of {@link PersistentCookieStore}
	 */
	public void setCookieStore(CookieStore cookieStore) {
		httpContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);
	}

	/**
	 * Sets the User-Agent header to be sent with each request. By default,
	 * "Android Asynchronous Http Client/VERSION (http://loopj.com/android-async-http/)" is used.
	 * @param userAgent the string to use in the User-Agent header.
	 */
	public void setUserAgent(String userAgent) {
		HttpProtocolParams.setUserAgent(this.httpClient.getParams(), userAgent);
	}

	/**
	 * Sets the connection time out. By default, 10 seconds
	 * @param timeout the connect/socket timeout in milliseconds
	 */
	public void setTimeout(int timeout){
		final HttpParams httpParams = this.httpClient.getParams();
		ConnManagerParams.setTimeout(httpParams, timeout);
		HttpConnectionParams.setSoTimeout(httpParams, timeout);
		HttpConnectionParams.setConnectionTimeout(httpParams, timeout);
	}

	/**
	 * Sets the SSLSocketFactory to user when making requests. By default,
	 * a new, default SSLSocketFactory is used.
	 * @param sslSocketFactory the socket factory to use for https requests.
	 */
	public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
		this.httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", sslSocketFactory, 443));
	}

	/**
	 * Sets headers that will be added to all requests this client makes (before sending).
	 * @param header the name of the header
	 * @param value the contents of the header
	 */
	public void addHeader(String header, String value) {
		if(TextUtils.isEmpty(value)){
			clientHeaderMap.remove(header);
		}else{
			clientHeaderMap.put(header, value);
		}
		
	}

	/**
	 * Sets basic authentication for the request. Uses AuthScope.ANY. This is the same as
	 * setBasicAuth('username','password',AuthScope.ANY) 
	 * @param username
	 * @param password
	 */
	public void setBasicAuth(String user, String pass){
		AuthScope scope = AuthScope.ANY;
		setBasicAuth(user, pass, scope);
	}

	/**
	 * Sets basic authentication for the request. You should pass in your AuthScope for security. It should be like this
	 * setBasicAuth("username","password", new AuthScope("host",port,AuthScope.ANY_REALM))
	 * @param username
	 * @param password
	 * @param scope - an AuthScope object
	 *
	 */
	public void setBasicAuth( String user, String pass, AuthScope scope){
		UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(user,pass);
		this.httpClient.getCredentialsProvider().setCredentials(scope, credentials);
	}

	private class InflatingEntity extends HttpEntityWrapper {
		public InflatingEntity(HttpEntity wrapped) {
			super(wrapped);
		}

		@Override
		public InputStream getContent() throws IOException {
			return new GZIPInputStream(wrappedEntity.getContent());
		}

		@Override
		public long getContentLength() {
			return -1;
		}
	}

	/**
	 * check has retry chance.
	 * @param exception
	 * @param executionCount
	 * @return
	 */
	public boolean retryRequest(IOException exception,int executionCount){
		return retryHandler.retryRequest(exception, executionCount, httpContext);
	}
	
	/**
	 * request remote server.
	 * @param rq
	 * @return
	 * @throws IOException
	 */
	public HttpResponse sendRequest(RequestParameter rq) throws IOException{
		HttpUriRequest uriRequest = null;
		switch (rq.getMethod()) {
		case RequestParameter.GET:
			uriRequest = new HttpGet(rq.getUrlWithQueryString());
			break;
		case RequestParameter.POST:
			HttpPost hp = new HttpPost(rq.getUrl());
			HttpEntity entity = rq.getEntity();
			if(entity != null){
				hp.setEntity(entity);
			}
			uriRequest = hp;
			break;
		case RequestParameter.DELETE:
			uriRequest = new HttpDelete(rq.getUrl());
			break;
		case RequestParameter.PUT:
			HttpPut hput = new HttpPut(rq.getUrl());
			HttpEntity entity2 = rq.getEntity();
			if(entity2 != null){
				hput.setEntity(entity2);
			}
			uriRequest = hput;
			break;
		default:
			uriRequest = new HttpGet(rq.getUrlWithQueryString());
			break;
		}
		return httpClient.execute(uriRequest,httpContext);
	}

	public void shutDown() {
		httpClient.getConnectionManager().shutdown();
	}
}
